fix(search-notes): say that only titles were searched when the result is empty - #114
Conversation
`searchContent` defaults to false and the two modes are exclusive: the
`whose` clause is built as either `name contains` or `body contains`,
never both. So the natural first call, `search-notes({query})`, returned
a bare `{"notes":[],"count":0}` for a term appearing in many note
bodies, with nothing in the response saying bodies were never read.
That is a silent false negative. The tool does not error, it reports
absence, and a caller reasonably concludes the note does not exist. On a
real library the term "because" matches 0 titles and 62 bodies.
The empty result now carries a hint that only titles were searched and
that `searchContent: true` searches bodies instead, using the same
disclosure mechanism as the applied result cap. Scoped to the empty
result, so a successful search is unchanged and no extra AppleScript
work is done. The default stays false and no search semantics change.
CLAUDE.md read "Set `searchContent: true` to search note body, not just titles", which describes an additive search. The modes are exclusive: `searchNotes` builds the `whose` clause as either `name contains` or `body contains`, never both, so bodies are searched instead of titles. This is the same misconception the empty-result hint in this PR exists to correct, stated in the file agents read before they call the tool, so fixing only the runtime message would leave the cause in place. README.md's parameter table was already accurate and is untouched.
|
Merged — thank you, @oliverames. This is a genuinely good catch and an unusually complete PR. The bug you found is the worst kind: Two things I especially appreciated:
Mirroring Shipping in v2.6.11. |
Description
search-notesmatches note titles unless the caller passessearchContent: true. When a title-only search matches nothing, the response is a bare{"notes":[],"count":0}with nothing in it saying that bodies were never read.I hit this as a caller. Asked to find notes on a topic, the natural first call is
search-notes({query: "<term>"}). I made that call for several terms that appear many times in note bodies and got an empty result each time. The obvious reading of that response is "no such note exists." It is wrong, and nothing in the response says so. I only noticed because an unrelated query matched a note whose title happened to contain the term inside a longer word, which revealed that matching was title-only. After that I enumerated folders by hand to find notes I already knew were there.That is a silent false negative, which is the worst failure mode for a search tool. It does not error, it confidently reports absence. On my own library the term
becausematches 0 titles and 62 bodies, so the empty result was hiding 62 notes.This PR adds one hint to the empty result:
Scope and cost:
searchContentstill defaults tofalse, and no search semantics change.describeSearchLimitdiscloses the applied result cap. New filesrc/utils/searchScope.tswith a sibling test, rather than wideningsearchLimit.ts, whose doc comment is specifically about the cap.The tool description does mention
searchContent, so this was never undocumented. But a description is not in front of the caller at the moment it is interpreting an empty result, and that is where the wrong conclusion gets drawn.A question for you, deliberately not implemented here
While tracing this I found that
searchContent: truesearches bodies instead of titles, not in addition to them.searchNotesbuilds thewhoseclause as eithername containsorbody containsand never both (src/services/appleNotesManager.ts), so there is no way to get title-or-body matches in one call. The wording of my hint says "instead" for exactly that reason.A
"title" | "body" | "both"mode looks like the real fix, but it haswhose-clause performance implications on a large library, and #100 already showed how tight the 30s budget is, so I did not want to bundle a guess about that into a disclosure PR. Two follow-on notes if you decide to pick it up:searchContent: truewith 0 results means titles were never searched. I deliberately left that case silent rather than expand this PR's scope. Say the word and I will make the hint symmetric.bothwould be a behavior change to an existing parameter, whereas a separatemodeparameter could keepsearchContentas a compatible alias.Happy to send that as its own PR if you want it, in whatever shape you prefer. Your call on scope.
Type of Change
Testing
pnpm test) — 518 tests across 20 files, including 3 new cases insrc/utils/searchScope.test.tscovering the hint on an empty title search, silence when titles matched, and silence when the caller already searched bodiespnpm run lint)pnpm run typecheck && pnpm run format:check)pnpm run build), and the committed bundle matches source (git diff --quiet build/after a rebuild)Also verified live, not just against mocks. I drove the built
build/index.jsover stdio against a real iCloud library and confirmed all three paths: a title-only search with no match now carries the hint, the same query withsearchContent: truedoes not, and a search that finds titles is unchanged apart from the existing limit disclosure.Checklist
One note on the version bump. I bumped to 2.6.11 and put the entry under
[Unreleased], matching what #104 did for 2.6.10, rather than opening a## [2.6.11]section. That keeps the existing unreleased items and this change filed together, since they will ship in the same release. If you would rather see a dated version heading, I am glad to restructure it.